
package playGround.dungeon;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import playGround.border.Border;
import playGround.square.Rock;
import playGround.square.Square;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;
import exceptions.IllegalPositionException;
import exceptions.SlipperinessOverflowException;

/**
 * @author Niels
 * @invar the map of positionSquares
 * 			|hasValidSquaresOnPosition()
 *
 */
public abstract class NonCompositeDungeon<S extends Square> extends Dungeon<S> {
	/**
	 * 
	 * @param maxDimensions the maxDimensions of this dungeon
	 * @effect	|super(maxDimensions)
	 */
	@Raw
	public NonCompositeDungeon(Position maxDimensions) {
		super(maxDimensions);
		positionSquares = new HashMap<Position,Square>();
	}
	/**
     * Variable referencing a map collecting all the Squares in this Dungeon and their Position.
	 */
	private Map<Position,Square> positionSquares;
	
	/**
	 * The Map of positions of the square
	 * @return a new HashMap with all the elements of the old one
	 */
	@Basic
	public Map<Position, Square> getPositionSquares(){
		return new HashMap<Position, Square>(positionSquares);
	}
	
	/**
	 * 
	 * @return ...
	 * 		| result == this.getPositionSquares().size()
	 * @note if the map contains more than Integer.MAXVALUE then it will return Integer.MAXVALUE
	 */
	public int getNbSquares(){
		return positionSquares.size();
	}
	
	/**
	 * This method searches the position of a square
	 * @param square
	 * @return ...
	 * 			| for each position in getPositionSquares().keySet()
	 * 			| if(getPositionSquares().get(position).equals(square))
	 * 			| then result == position
	 */
	public Position getPositionSquare(Square square){
		if(square != null){
			for(Position position: positionSquares.keySet()){
				if(positionSquares.get(position).equals(square))
					return new Position(position);
			}
		}
		return null;
	}
	/**
	 * adds a square to a specific position
	 * @param position on the specified position
	 * @param square to add
	 * @post ...
	 * 		| new.getPositionSquares().contains(position)
	 * 		| new.getPositionSquares().get(position) == square
	 * @post ...
	 * 		| new.getPositionSquares().size() == getPositionSquares().size()+1
	 * @effect ...
	 * 		| this.resolveBorder(position,square)
	 * @effect
	 * 		|this.resolveAreaAndBorderConflict(position,square)
	 * @throws IllegalArgumentException
	 * 		| !canHaveAsSquare(square)
	 * @throws IllegalPositionException
	 * 		| ! canHaveAsPosition(position)||positionHasSquare(position)
	 * @throws SlipperinessOverflowException
	 * 		| square.getSlipperyMaterial() && exceedMaxSlipperySquares(getNbSquares()+1)
	 * 
	 */
	public <E extends S> void addSquare(Position position, E square)throws IllegalArgumentException,IllegalPositionException,SlipperinessOverflowException{

		if(!canHaveAsSquare(square))
			throw new IllegalArgumentException();
		if(! canHaveAsPosition(position,null)||positionHasSquare(position)){
			throw new IllegalPositionException();
		}	
		if(square.getSlipperyMaterial()){
			if(exceedMaxSlipperySquares(getNbSquares()+1))
				throw new SlipperinessOverflowException();
		}
		resolveBorder(position,square);
		positionSquares.put(position, square);
		resolveAreaAndBorderConflict(position, square);
	}
	/**
	 * 
	 * @param position the position of the square
	 * @param square the square you want to resolve
	 * @effect ...
	 * 		| for each position in super.getRootDungeon().getPositionSquares().keySet()
	 * 		| if(position.getDirectionOf(position) != null)
	 * 		| super.getRootDungeon().getPositionSquares().get(position).controlBorder(square,direction)
	 * @effect ...
	 * 		|Map<Position,Square> positionSquares = super.getRootDungeon().getPositionSquares()
	 * 		| for each position in positionSquares.keySet()
	 * 		| if(positionSquares.get(position).temperatureIsDependentUponNeighbours())
	 * 		| positionSquares.get(position).calculateTemperature()
	 * @post ...
	 * 		| for each position in calculateArea(position, new HashSet<Position>())
	 * 		|	super.getRootDungeon().getPositionSquares().get(position).getSquaresOfthisAreaNotSeparatedWithBorder()==calculateArea(position, new HashSet<Position>())
	 * @pre ...
	 * 		|super.getRootDungeon().getPositionSquares().get(position)==square
	 */
	public void resolveAreaAndBorderConflict(Position position, Square square) {
		int nbOfNeighbors=0;
		Map<Position, Square> positionAllSquares = super.getRootDungeon().getPositionSquares();
		Position absolutePosition=this.getAbsolutePosition(position,this);
		for(Position existingPosition: positionAllSquares.keySet()){
			Direction direction = existingPosition.getDirectionOf(absolutePosition);
			Square theSquare = positionAllSquares.get(existingPosition);
			if(theSquare.temperatureIsDependentUponNeighbours()){
				Rock squareRock = (Rock) theSquare;
				squareRock.changeTemperature();
			}
			if( direction != null){
				Square squareOfList = positionAllSquares.get(existingPosition);
				squareOfList.controlBorder(square,direction);
				nbOfNeighbors++;
			}
		}
		if(nbOfNeighbors>1)
			fixArea(absolutePosition, square);
	}
	/**
	 * This method returns the absolutPosition of the square
	 * @return 
	 * 		| if(dungeon.getSuperDungeon() == null)
	 * 		| then result == position
	 * 		| else
	 * 		| then result == super.getSuperDungeon().getAbsolutePosition(position,this)
	 */
	@Override
	public Position getAbsolutePosition(Position position,Dungeon<? extends S> dungeon) {
		if(getSuperDungeon()!=null)
			return super.getSuperDungeon().getAbsolutePosition(position,this);
		return position;
	}
	/**
	 * This method resolves the borderConflict of a square
	 * @param 	position where a conflict is
	 * @param 	square that has the conflict
	 * @post	borderConflict is resolved
	 */
	public abstract void resolveBorder(Position position, Square square);
	/**
	 * 
	 * @param square
	 * @return
	 * 		| if(square == null || square.isTerminated())
	 * 		| then result == false
	 * 		| if(dungeonHasSquare(square))
	 * 		| then result == false
	 *		| for each border in square.getAllBorders().values()	
	 *		| if(border != null && border.getNSquares()>1)
	 *		| then result == false
	 */
	public boolean canHaveAsSquare(Square square){
		if(square==null)
			return false;
		if(dungeonHasSquare(square))
			return false;
		if(square.isTerminated())
			return false;
		for(Border border: square.getAllBorders().values()){
			if(border != null){
				if(border.getNbSquares()>1)
					return false;
			}
		}
		return true;
	}
	/**
	 * This method checks if the position is valid
	 * @return 
	 * 		| if(position.getXValue()==position.getYValue()&&position.getYValue()==position.getZValue())
	 * 		| then result == false
	 * 		| if(! super.canHaveAsPosition(position))
	 * 		| then result == false
	 * 		| if(super.getSuperDungeon()!=null&&(!super.getSuperDungeon().canHaveAsPosition(position,this)))
	 * 		| then result == false
	 */
	@Override
	public boolean canHaveAsPosition(Position position, Dungeon<? extends S> dungeon){
		if(! super.canHaveAsPosition(position))
			return false;
		if(position.getXValue()==position.getYValue()&&position.getYValue()==position.getZValue())
			return false;
		if(super.getSuperDungeon()!=null)
			if(!super.getSuperDungeon().canHaveAsPosition(position,this))
				return false;
		return true;
	}
	/**
	 * removes the square from a specific position
	 * @param position
	 * @post ...
	 * 			| new.getPositionSquares().size() = getPositionSquares().size()-1
	 * @post ...
	 * 			| ! (new.getPositionSquares().containskey(position))
	 * @post ...
	 * 			| (new getPositionSquares().get(positionToBeRemoved)).isTerminated()==true
	 * @effect	|for each direction in square.getAllBorders().keySet()
	 * 			|Position neighboringPosition = position.getNeighboringPosition(direction)
	 * 			|Square neighbouringSquare= this.positionSquares.get(neighboringPosition)
	 * 			|if(neighbouringSquare != null && neighbouringSquare.temperatureIsDependentUponNeighbours())
	 * 			|then ((Rock) neighbouringSquare).calculateTemperature()
	 * @post	|for each position in calculateArea(position, new HashSet<Position>())
	 * 			|super.getRootDungeon().getPositionSquares().get(position).getSquaresOfthisAreaNotSeparatedWithBorder()==calculateArea(position, new HashSet<Position>())
	 * @pre ...
	 * 		|super.getRootDungeon().getPositionSquares().get(position)==square
	 */
	public void removeSquare(Position positionToBeRemoved){
		if(positionToBeRemoved!=null){
			Square squareToBeRemoved = this.positionSquares.get(positionToBeRemoved);	
			squareToBeRemoved.terminate();
			this.positionSquares.remove(positionToBeRemoved);
			fixAreaAfterRemoval(positionToBeRemoved, squareToBeRemoved);
		}
	}
	/**
	 * this method removes a square from the dungeon
	 * @param square you want to remove
	 * @post ...
	 * 		| ! (new.getPositionSquares().containsValue(square))
	 * @post ...
	 * 		| new.getPositionSquares().size() = getPositionSquares().size() -1
	 * @post ...
	 * 		| (new square).isTerminated()==true
	 * @effect	|for each direction in square.getAllBorders().keySet()
	 * 			|Position neighboringPosition = position.getNeighboringPosition(direction)
	 * 			|Square neighbouringSquare= this.positionSquares.get(neighboringPosition)
	 * 			|if(neighbouringSquare != null && neighbouringSquare.temperatureIsDependentUponNeighbours())
	 * 			|then ((Rock) neighbouringSquare).calculateTemperature()
	 * @post	|for each position in calculateArea(position, new HashSet<Position>())
	 * 			|super.getRootDungeon().getPositionSquares().get(position).getSquaresOfthisAreaNotSeparatedWithBorder()==calculateArea(position, new HashSet<Position>())
	 * @pre ...
	 * 		|super.getRootDungeon().getPositionSquares().get(position)==square
	 */
	public void removeSquare(Square square){
		if(square != null){
			for(Iterator <Position> it = positionSquares.keySet().iterator(); it.hasNext();){
				Position selectedPosition = it.next();
				if(this.positionSquares.get(selectedPosition)==square){
					it.remove();
					square.terminate();
					fixAreaAfterRemoval(selectedPosition,square);
				}
			}
		}
	}
	/**
	 * this method makes sure that all the squares of this area have the newly added square to their set of squares
	 * @param position the position where you added the square
	 * @param square you added to this position
	 * @post ...
	 * 		| for each position in calculateArea(position, new HashSet<Position>())
	 * 		|	super.getRootDungeon().getPositionSquares().get(position).getSquaresOfthisAreaNotSeparatedWithBorder()==calculateArea(position, new HashSet<Position>())
	 * @pre ...
	 * 		|super.getRootDungeon().getPositionSquares().get(position)==square
	 */
	private void fixArea(Position position,Square square){
		assert(super.getRootDungeon().getPositionSquares().get(position)==square);
		Set<Position> allreadyDone= new HashSet<Position>();
		Set<Position> area= this.calculateArea(position, allreadyDone);
		Set<Square> squaresOfArea= new HashSet<Square>();
		for(Position pos:area){
			squaresOfArea.add(super.getRootDungeon().getPositionSquares().get(pos));
		}
		for(Square squareToChange: squaresOfArea){
			squareToChange.replaceSquaresOfArea(squaresOfArea);
		}
	}
	/**
	 * this method makes sure that all the squares have a correct area after you have removed a square
	 * @param position where you have removed a square
	 * @param square you have removed
	 * @effect	|for each direction in square.getAllBorders().keySet()
	 * 			|Position neighboringPosition = position.getNeighboringPosition(direction)
	 * 			|Square neighbouringSquare= this.positionSquares.get(neighboringPosition)
	 * 			|if(neighbouringSquare != null && neighbouringSquare.temperatureIsDependentUponNeighbours())
	 * 			|then ((Rock) neighbouringSquare).calculateTemperature()
	 * @post	|for each position in calculateArea(position, new HashSet<Position>())
	 * 			|super.getRootDungeon().getPositionSquares().get(position).getSquaresOfthisAreaNotSeparatedWithBorder()==calculateArea(position, new HashSet<Position>())
	 * @pre ...
	 * 		|super.getRootDungeon().getPositionSquares().get(position)==square
	 */
	private void fixAreaAfterRemoval(Position position, Square square){
		for(Direction direction: square.getAllBorders().keySet()){
			Position neighboringPosition = position.getNeighboringPosition(direction);
			Square neighbouringSquare= this.positionSquares.get(neighboringPosition);
			if(neighbouringSquare!=null){
				fixArea(neighboringPosition,neighbouringSquare);
			}
			if(neighbouringSquare != null && neighbouringSquare.temperatureIsDependentUponNeighbours()){
				Rock squareRock = (Rock) neighbouringSquare;
				squareRock.changeTemperature();
			}
		}
	}
	/**
	 * Recursive method to calculate area of the given position, assuming all doors are closed.
	 * @param position	The position of the square of which the area is about to get calculated.
	 * @param allreadyDone a set of Positions containing the positions
	 * @return 	|if(!this.positionHasSquare(position)
	 * 			|then result == null
	 * @return	|Map<Position, Square> positionAllSquares = super.getRootDungeon().getPositionSquares()
	 * 			|Square square=positionAllSquares.get(position)
	 * 			|Set<Position> area=new HashSet<Position>()
	 * 			|area.add(position)
	 * 			|allreadyDone.add(position)
	 * 			|for each direction of square.getAllBorders().keySet()
	 * 			|	Position neighboringPosition=position.getNeighboringPosition(direction)
	 * 			|	Square neighboringSquare=positionAllSquares.get(neighboringPosition)
	 * 			|	if(!allreadyDone.contains(neighboringPosition)&&neighboringSquare!=null)
	 * 			|		then if(square.getBorder(direction)==null)
	 * 			|			then area.add(neighboringPosition) and area.addAll(calculateArea(neighboringPosition,allreadyDone))
	 * 			|result==area				
	 */
	public Set<Position> calculateArea(Position position,Set<Position> allreadyDone){
		Map<Position, Square> positionAllSquares = super.getRootDungeon().getPositionSquares();
		if(!positionAllSquares.containsKey(position))
			return null;
		Square square=positionAllSquares.get(position);
		Set<Position> area=new HashSet<Position>();
		area.add(position);
		allreadyDone.add(position);
		for(Direction direction: square.getAllBorders().keySet()){
			Position neighboringPosition=position.getNeighboringPosition(direction);
			Square neighboringSquare=positionAllSquares.get(neighboringPosition);
			if(!allreadyDone.contains(neighboringPosition)&&neighboringSquare!=null){
				if(square.getBorder(direction)==null){
					area.add(neighboringPosition);
					area.addAll(calculateArea(neighboringPosition,allreadyDone));
				}
			}
		}
		return area;
	}	
	/**
	 * Method to add a border between two squares.
	 * @param position1 position of the square you want to build a border next to
	 * @param position2 position of second square
	 * @param border the border you want to add
	 * @throws IllegalArgumentException
	 * 			|!this.positionHasSquare(position1)||(!this.positionHasSquare(position2))||position1.getDirectionOf(position2)==null
	 * @post	|new.getPositionSquares().get(position1).getBorder(position1.getDirectionOf(position2))==border
	 * 			|new.getPositionSquares().get(position2).getBorder(position2.getDirectionOf(position1))==border
	 */
	public void addBorderBetweenTwoSquares(Position position1, Position position2, Border border) throws IllegalArgumentException{
		if(!this.positionHasSquare(position1)||(!this.positionHasSquare(position2)))
			throw new IllegalArgumentException();
		Square square1=this.getPositionSquares().get(position1);
		Square square2=this.getPositionSquares().get(position2);
		Direction direction1=position1.getDirectionOf(position2);
		if(direction1==null)
			throw new IllegalArgumentException();
		if(square1.getBorder(direction1)!=null){
			square1.removeBorder(position1.getDirectionOf(position2));
		}	
		square1.addBorder(position1.getDirectionOf(position2), border);
		square2.addBorder(position2.getDirectionOf(position1),border);
		fixArea(position1,square1);
		fixArea(position2,square2);
	}
	/**
	 * Method to remove border between two Squares.
	 * @param position1 position of square bordering the border you want to remove
	 * @param position2 position of the second square bordering the border you want to remove
	 * @throws IllegalArgumentException
	 * 			|!this.positionHasSquare(position1)||(!this.positionHasSquare(position2))||position1.getDirectionOf(position2)==null
	 * @post	|new.getPositionSquares().get(position1).getBorder(position1.getDirectionOf(position2))==null
	 * 			|new.getPositionSquares().get(position2).getBorder(position2.getDirectionOf(position1))==null
	 * @post	|new.getPositionSquares().get(position1).isInSameArea(new.getPositionSquares().get(position2))
	 * 			
	 */
	public void removeBorderBetweenTwoSquares(Position position1, Position position2)throws IllegalArgumentException{
		if(!this.positionHasSquare(position1)||(!this.positionHasSquare(position2)))
			throw new IllegalArgumentException();
		Square square1=this.getPositionSquares().get(position1);
		Square square2=this.getPositionSquares().get(position2);
		Direction direction1=position1.getDirectionOf(position2);
		if(direction1==null)
			throw new IllegalArgumentException();
		square1.removeBorder(direction1);
		square2.addSquaresToArea(square1.getSquaresOfthisAreaNotSeparatedWithBorder());
	}
	/**
	 * checks whether it has valid squares on all positions
	 * @return
	 * 		| if(exceedMaxSlipperySquares(getNbSquares()))
	 * 		| then result == false
	 * @return
	 * 		| for each position in getPositionSquares().keySet()
	 * 		| if(!canHaveAsPosition(position))
	 * 		| then result == false
	 * @return
	 * 		| for each square in getPositionSquares().values()
	 * 		| if(! canHaveAsSquare(square))
	 * 		| then result == false
	 */
	public boolean hasValidSquaresOnPosition(){
		if (exceedMaxSlipperySquares(getNbSquares()))
			return false;
		
		for(Position position: positionSquares.keySet()){
			if(! canHaveAsPosition(position))
				return false;
		}
		for(Square square: positionSquares.values()){
			if(! canHaveAsSquare(square))
				return false;
		}
		return true;
	}
	/**
	 * returns whether this position already contains a square
	 * @param position
	 * @return
	 * 		|if(position == null)
	 * 		| result == false
	 * 		|else if(getPositionSquares().containsKey(position))
	 * 		| result == true
	 * 		
	 */
	public boolean positionHasSquare(Position position){
		if(position == null)
			return false;
		return positionSquares.containsKey(position);
	}
	/**
	 * Checks whether or not the dungeon exceeds the maxSlippery limit
	 * @param NbSquares in the map
	 * @return	| totalCount=0
	 * 			| for each square in getPositionSquares().values()
	 * 			| 	if(square.getSlipperyMaterial() == true)
	 * 			| 		then totalCount++
	 * 			| if(totalCount <= getMaxSlipperySquares())
	 * 			| then result == false
	 */
	public boolean exceedMaxSlipperySquares(int NbSquares){
		BigDecimal countSlippery = new BigDecimal(NbSquares);
		for(Square square: positionSquares.values()){
			if(! square.getSlipperyMaterial())
				countSlippery = countSlippery.subtract(new BigDecimal(1));
		}
		BigDecimal slipperyDungeon = countSlippery.multiply(new BigDecimal(100)).divide(new BigDecimal(NbSquares),2);
		BigDecimal maxSlippery = getMaxSlipperySquares().multiply(new BigDecimal(100));
		if( slipperyDungeon.doubleValue()<= maxSlippery.doubleValue())
			return false;
		return true;
		
	}
	/**
	 * This method returns the shaft and levels from the dungeon.
	 * @return 
	 * 		| return result.contains(this)
	 */
	public Set<Dungeon<? extends S>> getShaftsAndLevels(){
		Set<Dungeon<? extends S>> result=new HashSet<Dungeon<? extends S>>();
		result.add(this);
		return result;
	}
	/**
	 * returns whether or not the dungeon already has this square
	 * @param square
	 * @return
	 * 		| getPositionSquares().containsValue(square)
	 */
	public boolean dungeonHasSquare(Square square) {
		return positionSquares.containsValue(square);
	}
	/**
	 *@post ..
	 *	| new.getPositionSquares.isEmpty()==true
	 *@effect
	 *		|super.terminate()
	 *@post ...
	 *		|for each square in this.getPositionSquares().values()
	 *		| square.terminate()
	 */
	@Override
	public void terminate(){
		for(Square square: positionSquares.values())
			square.terminate();
		positionSquares.clear();
		super.terminate();
	}
	/**
	 * This method returns an iterator for getting squares
	 * @return
	 * 		| square = new NonCompositeDungeonIterator(this)
	 * 		|if(square.hasNext() && topDungeon)
	 * 		|then squares.next
	 * 		|result == squares
	 */
	public Iterator<Object> getSquares(){
		
		Iterator<Object> squares = new Iterator<Object>(){
			{
				elementsDone = -1;
				maxNumberSquares = getPositionSquares().size();
				allSquares = new ArrayList<Square>(getPositionSquares().values());
			}
			private ArrayList<Square> allSquares;
			private int elementsDone;
			private int maxNumberSquares;
			@Override
			public boolean hasNext() {
						return (elementsDone+1)<maxNumberSquares;
			}
			@Override
			public Object next() throws NoSuchElementException{
				if(! hasNext())
					throw new NoSuchElementException();
				elementsDone++;
					return allSquares.get(elementsDone);
			}
			@Override
			public void remove() {
			}
		};
		int count = 0;
		if(count == 0)
			count = 1;
		else if(squares.hasNext() && count==1){
			squares.next();
			count = 1;
		}
		return squares;	
	}
	
}


